import * as lsp from "vscode-languageserver";
import { TextDocument } from "vscode-languageserver-textdocument";
import { URI } from "vscode-uri";
import { DefaultOptions, DsWindowProperties } from "../common/types";
import { hasProperty } from "../common/util";
import { getGlobalDefinitionPath, t } from "./analysis";
import { Project } from "./project";
import { createTextDocuments } from "./textDocument";
import { uriToPath } from "./translation";
import { getProjectRootPath } from "./util";

export interface ProjectWorkspace extends lsp.Disposable {
    readonly documentsCount: number;
    readonly rootPath: string;
    readonly project: Project;
    readonly keep: boolean;
    readonly onDocumentsClosed: lsp.Event<ProjectWorkspace>;
    getTextDocument(fsPath: string): TextDocument | undefined;
    updateTextDocument(fsPath: string, changes: lsp.TextDocumentContentChangeEvent[]): void;
    openTextDocument(fsPath: string, document: lsp.TextDocumentItem): boolean;
    closeTextDocument(fsPath: string): TextDocument | undefined;
    createTextDocument(fsPath: string): void;
    deleteTextDocument(fsPath: string): void;
    toUri(fsPath: string): URI;
    forEach(callback: (e: TextDocument) => any, thisArg?: any): void;
}

export interface ProjectWorkspaceManager extends lsp.Disposable {
    readonly workspaceRoots: readonly string[];
    readonly onLogMessage: lsp.Event<string>;
    updateConfiguration(configuration: DsWindowProperties): void;
    isLoading(fsPath: string): boolean;
    getWorkspace(fsPath: string): ProjectWorkspace | undefined;
    getWorkspaceRoot(fsPath: string): string;
    createWorkspace(project: Project): Promise<ProjectWorkspace>;
    createWorkspace(fsPath: string, keep: boolean): Promise<ProjectWorkspace>;
    createWorkspace(fsPath: string, documentsCount: number): Promise<ProjectWorkspace>;
    createWorkspace(fsPath: string, openedDocuments: { fsPath: string, item: lsp.TextDocumentItem }[]): Promise<ProjectWorkspace>;
    closeWorkspace(fsPath: string): void;
}

export interface ProjectInitializer {
    (rootPath: string): Promise<Project>;
}

export interface ProjectWorkspaceOptions {
    rootPath: string;
    initialDocumentsCount?: number;
    openedDocuments?: { fsPath: string, item: lsp.TextDocumentItem }[];
    projectInitializer: ProjectInitializer | Project;
    keep?: boolean;
}

export async function createProjectWorkspace(options: ProjectWorkspaceOptions): Promise<ProjectWorkspace> {
    let rootPath = options.rootPath;
    let keep = options.keep !== undefined ? options.keep : false;
    let documentsCount = options.initialDocumentsCount !== undefined ? options.initialDocumentsCount : 1;
    let project = typeof options.projectInitializer === "function" ? await options.projectInitializer(rootPath) : options.projectInitializer;
    let documentsClosedEmitter = new lsp.Emitter<ProjectWorkspace>();
    let documents = createTextDocuments();
    let openedDocument = new Set<string>();
    if (options.openedDocuments) {
        for (const doc of options.openedDocuments) {
            openedDocument.add(doc.fsPath);
            documents.open(doc.fsPath, doc.item);
        }
        documentsCount = options.openedDocuments.length;
    }

    let workspace: ProjectWorkspace = {
        rootPath,
        project,
        keep,
        documentsCount,
        onDocumentsClosed: documentsClosedEmitter.event,
        updateTextDocument,
        openTextDocument,
        closeTextDocument,
        createTextDocument,
        deleteTextDocument,
        getTextDocument: documents.get,
        toUri: documents.toUri,
        forEach: documents.forEach,
        dispose,
    };

    documents.onTextDocumentOpened(e => {
        openedDocument.add(e.uri);
        documentsCount++;
    });

    documents.onTextDocumentClosed(e => {
        openedDocument.delete(e.uri);
        documentsCount--;
        if (documentsCount === 0 && !keep) {
            documentsClosedEmitter.fire(workspace);
        }
    });

    documents.onTextDocumentChanged(e => {
        let fsPath = uriToPath(e.uri);
        project.program.update(fsPath, { text: e.getText(), changes: [] });
        project.program.parse(fsPath);
    });

    return workspace;

    function updateTextDocument(fsPath: string, changes: lsp.TextDocumentContentChangeEvent[]): void {
        let document = documents.update(fsPath, changes);
        if (document) {
            let params: t.TextChange[] = [];
            for (let i = changes.length - 1; i >= 0; i--) {
                const item = changes[i];
                if (hasProperty(item, "range")) {
                    let change = item as {
                        range: lsp.Range;
                        rangeLength?: number | undefined;
                        text: string;
                    };
                    let range = change.range;
                    let start = document.offsetAt(range.start);
                    let length = change.rangeLength;
                    if (length === undefined) {
                        length = document.offsetAt(range.end) - start;
                    }
                    params.push({ span: { start, length }, text: item.text });
                }
                else {
                    params = [];
                    break;
                }
            }
            project.program.update(fsPath, {
                text: document.getText(),
                changes: params
            });
        }
    }

    function openTextDocument(fsPath: string, document: lsp.TextDocumentItem): boolean {
        project.program.activateFile(fsPath);
        return documents.open(fsPath, document);
    }

    function closeTextDocument(fsPath: string): TextDocument | undefined {
        project.program.closeFile(fsPath);
        return documents.close(fsPath);
    }

    function createTextDocument(fsPath: string): void {
        project.program.addFile(fsPath);
        project.program.refresh();
    }

    function deleteTextDocument(fsPath: string): void {
        project.program.deleteFile(fsPath);
        project.program.refresh();
    }

    function dispose() {
        project.dispose();
        documents.dispose();
        openedDocument.clear();
        documentsClosedEmitter.dispose();
    }

}

export function createProjectWorkspaceManager(projectInitializer: ProjectInitializer): ProjectWorkspaceManager {
    let workspaceFolderNames = DefaultOptions.workspaceFolderNames.slice();
    const logMessageEmitter = new lsp.Emitter<string>();
    // key是小写的根路径
    const workspaces = new Map<string, ProjectWorkspace>();
    const workspaceRoots: string[] = [];
    const loadingWorkspace = new Set<string>();

    return {
        workspaceRoots,
        updateConfiguration,
        onLogMessage: logMessageEmitter.event,
        isLoading,
        getWorkspace,
        getWorkspaceRoot,
        createWorkspace,
        closeWorkspace,
        dispose() {
            workspaces.forEach(space => space.dispose());
            workspaces.clear();
        }
    };

    function updateConfiguration(configuration: DsWindowProperties): void {
        if (configuration.workspaceFolderNames) {
            workspaceFolderNames = configuration.workspaceFolderNames.split(";");
        }
    }

    function getWorkspaceRoot(fsPath: string): string {
        let global = getGlobalDefinitionPath().toLowerCase();
        if (fsPath.toLowerCase().startsWith(global)) {
            return global;
        }
        return getProjectRootPath(fsPath, workspaceFolderNames);
    }

    function getWorkspace(fsPath: string): ProjectWorkspace | undefined {
        let rootPath = getWorkspaceRoot(fsPath);
        let workspace = workspaces.get(rootPath.toLowerCase());
        if (!workspace) {
            return undefined;
        }
        if (workspaceRoots[0] !== rootPath) {
            workspaceRoots.splice(workspaceRoots.indexOf(rootPath), 1);
            workspaceRoots.unshift(rootPath);
        }
        return workspace;
    }

    function isLoading(fsPath: string): boolean {
        let rootPath = getWorkspaceRoot(fsPath);
        return loadingWorkspace.has(rootPath.toLowerCase());
    }

    async function createWorkspace(arg1: string | Project, arg2?: { fsPath: string, item: lsp.TextDocumentItem }[] | number | boolean): Promise<ProjectWorkspace> {
        let workspace: ProjectWorkspace | undefined;
        let rootPath: string;
        if (typeof arg1 !== "string") {
            rootPath = arg1.rootPath;
            workspace = workspaces.get(arg1.rootPath.toLowerCase());
            if (workspace) {
                return workspace;
            }
            let lowerPath = arg1.rootPath.toLowerCase();
            loadingWorkspace.add(lowerPath);
            workspace = await createProjectWorkspace({
                projectInitializer: arg1,
                rootPath: arg1.rootPath,
                keep: true,
            });
            loadingWorkspace.delete(lowerPath);
            workspaces.set(lowerPath, workspace);
            return workspace;
        }
        rootPath = getProjectRootPath(arg1, workspaceFolderNames);
        let lowerPath = rootPath.toLowerCase();
        workspace = workspaces.get(lowerPath);
        if (workspace) {
            return workspace;
        }
        loadingWorkspace.add(lowerPath);
        logMessageEmitter.fire(`载入项目 (${rootPath}) ...`);
        let openedDocuments: { fsPath: string, item: lsp.TextDocumentItem }[] | undefined;
        let documentsCount: number | undefined;
        let keep: boolean | undefined;
        if (typeof arg2 === "number") {
            documentsCount = arg2;
        }
        else if (typeof arg2 === "boolean") {
            keep = arg2;
        }
        else {
            openedDocuments = arg2;
        }
        workspace = await createProjectWorkspace({
            rootPath,
            projectInitializer,
            openedDocuments,
            initialDocumentsCount: documentsCount,
            keep,
        });
        workspace.onDocumentsClosed(listener => {
            workspaces.delete(listener.rootPath.toLowerCase());
            workspaceRoots.splice(workspaceRoots.indexOf(listener.rootPath), 1);
            logMessageEmitter.fire(`卸载项目 (${listener.rootPath})`);
        });
        loadingWorkspace.delete(lowerPath);
        workspaces.set(lowerPath, workspace);
        workspaceRoots.unshift(rootPath);
        return workspace;
    }

    function closeWorkspace(fsPath: string): void {
        let rootPath = getProjectRootPath(fsPath, workspaceFolderNames);
        let workspace = workspaces.get(rootPath.toLowerCase());
        if (workspace) {
            workspaces.delete(rootPath.toLowerCase());
            workspace.dispose();
            logMessageEmitter.fire(`卸载项目 (${rootPath})`);
        }
    }

}

